# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
## NPSolitaire:
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## from PySolFC code:
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Atomic move mix-in support. Handles undo and redo of game moves.
"""
__docformat__ = 'restructuredtext en'

import abc
from collections import Counter
import logging

LOGGER = logging.getLogger('PySolFC.GameLib.MoveMixin')

from . import state
from ..animation import AnimateWin, draw_hint_path
from ..hint import MoveHint, DefaultHint
from ..pilelib import move as mv
from ..pysolaudio import AUDIO_DEV
from ..quickplayscore import Normal as Normal_Quickplay
from ..settings import CONF_OPT, REDO, UNDO
from ..statdb import STAT_DB
from ..viewlib.dialog import Dialog


class GameMove(object):
    """
     Handle moves with move history for undo/redo. Actual move handled by atomic
     objects that perform on low-level pile methods.

     *note:*
       - Use only play methods when calling from outside a pile object. A pile
         may add important triggers for a movement that should not be overrode.
       - Use do methods for pile move methods only.
    """
    __metaclass__ = abc.ABCMeta
    __status = None

    S_INIT = 0x00
    S_DEAL = 0x10
    S_FILL = 0x20
    S_RESTORE = 0x30
    S_PLAY = 0x40
    S_UNDO = 0x50
    S_REDO = 0x60
    S_FINISH = 0x90

    # Allow for move hint of best next move.
    _hint_ = DefaultHint
    _hint_item_ = draw_hint_path

    # Allow for quick play of selected card.
    _quickplay_ = Normal_Quickplay

    s = {}
    allstacks = []
    top = is_preview = random = canvas = nextgame = images = talon = None

    @abc.abstractmethod
    def new(self, random=None, restart=False, autoplay=True):
        """
        Stub to start a new card game. Satisfied with module in library.

        .. new(random:Random=None, restart:bool=False, autoplay:bool=True)
             -> None
        """
        pass

    @abc.abstractmethod
    def _view_update(self, mode='all'):
        """
        Update canvas view.

        Call each *Stack._update_text()* then *_update_text()* to update
        canvas labels. Update text on status bar and states on menubar.
        """
        pass

    @abc.abstractmethod
    def _timer_update(self, after=True):
        """
        Update timer and status bar for current game. Satisfied with module in
        library.

        .. \_timer_update(after:bool=True)
        """
        pass

    @abc.abstractmethod
    def _timer_stop(self, after=True):
        """
        Stub to stop timer for current game. Satisfied with module in library.

        .. \_timer_stop() -> None.
        """
        pass

    @property
    def is_demo(self):
        """
        Stub to return current demo state.  Satisfied with demo module in
        library.

        .. is_demo -> bool
        """
        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'is_demo'):
            return ancestor.is_demo

    @property
    def demo_start(self, mixed=True):
        """
        Stub to start demo. Satisfied with demo module in library.

        .. demo_start([mixed:bool=True]) -> None
        """
        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'demo_start'):
            return ancestor.demo_start(mixed=mixed)

    @property
    def status(self):
        """
        Get status details.

        Used for games with additional values that need persistence. Useful for
        games that restrict play to certain events before another can happen.

        .. status -> *
        """
        return self.__status

    @status.setter
    def status(self, status):
        """
        Set status details with history.

        Used for games with additional values that need persistence. Useful for
        games that restrict play to certain events before another can happen.

        .. status = *

        *Parameters*:
            status: {any}
              Set given value[s] as sequence or mapping that need to persist.
        """
        self._move_do(mv.ASaveStatusMove(self.status, UNDO))
        self.__status = status
        self._move_do(mv.ASaveStatusMove(self.status, REDO))

    def set_status(self, status):
        """
        Set status details without history.

        Used for atomic move support.

        .. set_status(status:*) -> None

        *Parameters*:
            status: {any}
              Set given value[s] as sequence or mapping that need to persist.
        """
        self.__status = status

    @property
    def move_state(self):
        """
        Return move state.

        .. move_index -> int
        """
        return self._move['state']

    @move_state.setter
    def move_state(self, value):
        """
        Set move state.

        Verify value is a move state then set.

        .. move_index = int
        """
        assert value in (self.S_INIT, self.S_DEAL, self.S_FILL, self.S_RESTORE,
                         self.S_PLAY, self.S_UNDO, self.S_REDO, self.S_FINISH,)
        if self._move['state'] != self.S_FINISH or value == self.S_INIT:
            self._move['state'] = value

    @property
    def move_index(self):
        """
        Return move index.

        .. move_index -> int
        """
        return self._move['index']

    @property
    def move_demo(self):
        """
        Return player moves.

        .. move_player -> int
        """
        return self._move['counter']['demo']

    @property
    def move_player(self):
        """
        Return player moves.

        .. move_player -> int
        """
        return self._move['counter']['player']

    @property
    def move_dupe(self):
        """
        Return duplicate moves.

        .. move_dupe -> int
        """
        return self._move['counter']['dupe']

    @property
    def move_total(self):
        """
        Return sum of all moves.

        .. move_total -> int
        """
        return sum(self._move['counter'][k]
                   for k in ['undo', 'redo', 'player', 'demo'])
    @property
    def move_dict(self):
        """
        Return move mapping for debug dialog.

        .. move_dict -> dict
        """
        return {'moves': self._move['index'],
                'undo': self._move['counter']['undo'],
                'bookmark': self._move['counter']['bookmark'],
                'demo': self._move['counter']['demo'],
                'play': self._move['counter']['player'],
                'total': self.move_total,
                'hints': self._move['counter']['hints'],
                'piles': self._move['counter']['piles'],
                'cards': self._move['counter']['cards'],
                'match': self._move['counter']['samerank'],}

    @property
    def is_perfect(self):
        """
        Return game perfect.

        Default move contain undo, bookmark, or shuffles or any highlights.

        .. is_success -> int
        """
        cnt = self._move['counter']
        return all(cnt[k] == 0 for k in ['undo', 'bookmark', 'shuffle',
                                        'piles', 'cards', 'samerank'])

    @property
    def is_success(self):
        """
        Return game success.

        Default 0: incomplete or game had hints or demo; 1: complete and not
        perfect; 2: complete and perfect.

        .. is_success -> int
        """
        cntr = self._move['counter']
        if not self.is_complete or any(cntr[k] > 0 for k in ['hints', 'demo']):
            return 0
        return 1 + int(self.is_perfect)

    @abc.abstractproperty
    def is_complete(self):
        """
        Return game complete.

        Default s[foundations] contain all cards.

        .. is_complete -> bool
        """
        pass

    @property
    def is_finish(self):
        """
        Return game finish.

        .. is_finish -> bool
        """
        return bool(self.move_state == self.S_FINISH)

    @property
    def can_undo_move(self):
        """
        Return undo game move.

        Default when config option undo and moves index > 0.

        .. can_undo_move -> bool
        """
        return bool(CONF_OPT['undo'] and self._move['index'] > 0)

    @property
    def can_play_sound(self):
        """
        Return play sound for move.

        Default when not S_DEAL or is demo mode.

        .. can_play_sound -> bool
        """
        play_sound = True
        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'can_play_sound'):
            play_sound = ancestor.can_play_sound

        return not self._move['state']  == self.S_DEAL and play_sound

    def __init__(self):
        """
        __init__(self) -> None

        Initialize a game with move snapshots. Then call ancestor method.

        *Variables:*
          snapshots : {Set}
            Initialize set to hold list of hashes of Stacks and their Cards.
        """
        self._move = {'state': self.S_PLAY, 'history': [], 'index': 0,
                      'current': [], 'counter': Counter(), 'snapshots': set()}

        self.win_animation = self.move_hint = None
        super(GameMove, self).__init__()

    def reset(self, restart=False):
        """
        Set new MoveHint. Call move reset method. Then call next method in order

        .. reset(restart:bool=False) -> None

        *Variables:*
          move_hints : {dict}
            Init MoveHint with _hint_ attribute.
        """
        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'reset'):
            ancestor.reset(restart)
        self.move_hint = MoveHint(self, self._hint_)
        self._move_reset()
        self.__status = None

    @state.game_busy
    def restore(self, data, reset=True):
        """
        Restore prior game details.

        Augemnt to set move history and snapshots. Call next method in order.

        .. restore(data:GameData[, reset:bool=True, autoplay:bool:True]) -> None

        *Parameters*:
          data: {GameData}
            Data to restore game move history and snapshot details.
          reset: {boolean}
            Reset current game before restore.

        *Variables:*
          moves : {dict}
            Set to passed game moves history.
        """
        self._move = data['move']
        del data['move']
        self.__status = data['status']
        del data['status']

        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'restore'):
            ancestor.restore(data, reset)

    def _move_reset(self):
        """
        Reset moves history and clear stats.

        .. \_move_reset() -> None

        *Variables:*
          moves : {dict}
            Reset game moves history. Set state to S_PLAY state. Clear history,
            index, current and counters for new game.
        """
        self._move = {'state': self.S_PLAY, 'history': [], 'index': 0,
                      'current': [], 'counter': Counter(), 'snapshots': set()}
        self._append_snapshot()

    def _move_do(self, atomic_move, reset=False):
        """
        Perform low-level atomic move. Record move in current sequence when
        state S_DEAL, S_FILL, S_RESTORE, or S_PLAY. Reset move hints when reset.

        *note:* atomic move objects will call low-level pile methods not the
          game-level move_pile or flip card methods.

        .. \_move_do(atomic_move:AtomicMove[, reset:bool=False]) -> None

        *Variables:*
          moves : {map}
            append current move to sequence when in state.
        """
        atomic_move.do(self)
        if self.S_DEAL <= self._move['state'] <= self.S_PLAY:
            self._move['current'].append(atomic_move)
        if reset:
            self.move_hint.reset()

    def deal(self, from_stack, to_stack, flip=True, sound=True, **kwargs):
        """
        Play from stack to build on to stack then flip with sound fx. Calls pile
        methods for move.

        *note:*
          - Use with method requiring a play and flip without finish move.
          - Like to do card flip and move but as 2 moves instead of 1.

        .. deal(from_stack:Pile, to_stack:Pile[, flip:bool=True,
             sound:bool=True, **animation keywords**]) -> None
        """
        if self.can_play_sound and sound:
            AUDIO_DEV.play('move', 30)
        from_stack.move_pile(1, to_stack, **kwargs)

        if flip:
            if self.can_play_sound and sound:
                AUDIO_DEV.play('flip', 5)
            to_stack.flip_card(**kwargs)

    def flip(self, stack, sound=True, **kwargs):
        """
        Flip top card from stack with sound fx. Perform auto play and finish
        move after. Calls pile methods for move.

        .. flip(stack:Pile[, sound:bool=True, **animation keywords**]) -> None
        """
        if self.can_play_sound and sound:
            AUDIO_DEV.play('flip', 5)

        stack.flip_card(**kwargs)
        self.auto_play()
        self.log()

    def move(self, ncards, from_stack, to_stack, sound=True, **kwargs):
        """
        Play ncards from top of from stack building on to stack with sound fx.
        Perform auto play and finish move after. Calls pile methods for move.

        .. move(ncards:int, from_stack:Pile, to_stack:Pile[,sound:bool=True,
             **animation keywords**]) -> None
        """
        if self.can_play_sound and sound:
            if to_stack in self.s['foundations']:
                AUDIO_DEV.play('drop', priority=30)
            else:
                AUDIO_DEV.play('move', priority=10)
        from_stack.move_pile(ncards, to_stack, **kwargs)

        self.auto_play()
        self.log()

    def fill(self, from_stack, to_stack, flip=True, sound=True,
                       **kwargs):
        """
        Flip then play top card from stack to build on to stack with sound fx.
        Perform auto play and finish move after. Calls pile methods for move.

        *note:*
          - Use with method requiring a flip and play with finish move.
          - Like do card flip and move but as seperate moves.

        .. fill(from_stack:Pile, to_stack:Pile[, flip:bool=True,
                sound:bool=True, **animation keywords**]) -> None
        """
        if flip:
            if self.can_play_sound and sound:
                AUDIO_DEV.play('flip', priority=5)
            to_stack.flip_card(**kwargs)

        if self.can_play_sound and sound:
            AUDIO_DEV.play('move', priority=10)
        from_stack.move_pile(1, to_stack, **kwargs)

        self.auto_play()
        self.log()

    def talon_deal(self, sound=True):
        """
        Deal cards from talon.

        When can deal, call talon pile deal cards method then call auto play.
        Return number of cards dealt. Deal is wrapped by a call to finish move.

        .. talon_deal(sound=True) -> int
        """
        if self.talon and self.talon.can_deal_cards():
            self.log()
            ncards = self.talon.deal_cards(sound=sound)
            self.log()
            self.auto_play()
            return ncards
        return 0

    def talon_redeal(self, sound=True):
        """
        Redeal cards from talon.

        Stub to redeal cards after 1st round. Called by talon pile redeal method
        after cards returned to refill stacks. Return number cards dealt.

        .. talon_redeal(self, sound=True) -> int
        """
        pass

    @abc.abstractmethod
    def _start(self, *kws):
        """
        Start the game (eg. deal initial cards).

        .. _start(self, *kws):
        """
        pass

    def _log_move(self, move):
        """
        Finish given move sequence then append new snapshot.

        .. \_log_move() -> None

        *Variables:*
          moves : {map}
            Update current moves stats.
        """
        redo = False  # Detect redo move to keep history
        if move['index'] + 1 < len(move['history']):
            c_move = move['history'][move['index']]
            if len(move['current']) == len(c_move):
                redo = all(a1 == a2 for a1, a2 in zip(move['current'], c_move))
        if redo:  # update existing entry due to possible minor changes
            LOGGER.info('detected redo: %s', move['current'])
            move['history'][move['index']] = move['current']
            move['index'] += 1
        else:  # resize (i.e. possibly shorten list from previous undos)
            move['history'][move['index']:] = [move['current']]
            move['index'] += 1
            assert move['index'] == len(move['history'])
        self._append_snapshot()

    def log(self):
        """
        Log current move.

        Update move stats for player when state not S_INIT. Call move update
        method for actual move. Clear current move sequence and update texts.
        Generate update menu event when not preview. Final update timer.

        .. log() -> None

        *Variables:*
          moves : {map}
            Update current moves stats. Reset current move seq.
        """
        self._timer_update(after=False)
        move = self._move
        if move['current']:
            return False
        self.move_hint.reset()
        # update stats
        if move['state'] != self.S_INIT:
            if self.is_demo:  # update stats
                move['counter']['demo'] += 1
                if move['index'] == 0:  # clear player moves
                    move['counter']['player'] = 0
            else:
                move['counter']['player']  += 1
                if move['index'] == 0:  # clear demo moves
                    move['counter']['demo'] = 0
        # update history
        self._log_move(self._move)
        move['current'] = []
        # update view
        self._view_update('move')
        return True

    def changed(self, restart=False):
        """
        Return game in progress.

        Used when quitting or loading games to prevent accidental loss.

        .. changed([restart:bool=False) -> bool
        """
        ng_counter = self.nextgame['counter']
        if self.is_finish or ng_counter['load'] > 0:
            return False     # game finished or loaded prior
        if not restart and any(ng_counter[k] for k in ['restart', 'bookmark']):
            return True      # game restarted or bookmarked
        if self._move['index'] == 0 or self._move['counter']['player'] == 0:
            return False     # game has no moves played
        return True

    def _auto_fill(self, pile):
        """
        Fill empty piles that rules require.

        Stub called from pile fill method to add cards after a move.

        .. \_auto_fill(pile:Pile) -> None

        *Parameter:*
          pile: {pile}
        """
        pass

    @abc.abstractmethod
    def _autopiles(self, event=None):
        """
        Return piles for auto or quick move. (autoflip, autodrop, quickplay)

        .. \_autopiles(self, event=None) -> seq(seq(Pile ...), seq(Pile ...),
             seq(Pile ...))
        """
        pass

    @state.not_state('is_demo')
    @state.cancel_drag(True)
    def auto_play(self, flip=None, drop=None, deal=None, sound=True):
        """
        Handle auto flip, drop, and deal/play.

        Using auto piles attribute, perform able move methods and update stats.
        Return when finished or all able perform.

        .. \auto_play(flip:bool=OPT_CONF, drop:bool=OPT_CONF,
             deal:bool=OPT_CONF, sound:bool=True) -> None

        *Parameters:*
          flip: {bool} OPT_CONF[autofaceup]
            auto flip cards able faceup.
          drop: {bool} OPT_CONF[autodrop]
            auto drop cards able foundation.
          deal: {bool} OPT_CONF[autodeal]
            auto deal talon on waste.

        *Variables:*
          moves :
            update move stats to update autoplay counter.
        """
        if self._check_finished():
            return
        flip_piles, drop_piles = self._autopiles()[:2]

        def _auto_drop_iter(piles):
            """
            Iterate piles that can drop.
            """
            for pile in piles:
                to_pile, ncards = pile.cardsDrop(self.s['foundations'])
                if to_pile:
                    yield pile, to_pile, ncards

        # a) flip cards
        if (flip or CONF_OPT['autofaceup']) and flip_piles:
            for pile in (s for s in flip_piles if s.cardFlips()):
                if self.can_play_sound and sound:
                    AUDIO_DEV.play('autoflip', priority=5)
                pile.flip_card()
                self.log()  # allow flip to be undone after
                self._move['counter']['autoplay'] += 1
                if self._check_finished():
                    return

        # b) drop cards
        if (drop or CONF_OPT['autodrop']) and drop_piles:
            for from_pile, to_pile, ncards in _auto_drop_iter(drop_piles):
                self.log()  # allow drop to be undone before
                if  self.can_play_sound and sound:
                    AUDIO_DEV.play('autodrop', priority=30)
                from_pile.move_pile(ncards, to_pile)
                self._move['counter']['autoplay'] += 1
                if self._check_finished():
                    return

        # c) deal cards
        if (deal or CONF_OPT['autodeal']) and self._auto_deal(sound=sound):
            self.log()    # allow deal to be undone after
            self._move['counter']['autoplay'] += 1
            self._check_finished()

    def _auto_deal(self, sound=True):
        """
        Auto deal/play card to empty waste.

        Return move done. Called by auto play method. May overide behavior.

        .. \_auto_deal([sound:bool=True]) -> bool
        """
        waste = self.s['wastes']
        if (waste and len(waste[-1].cards) == 0 and
            self.talon.can_deal_cards()):
            return bool(self.talon_deal(sound=self.can_play_sound and sound))
        return False

    def quick_play(self, pile, event=None):
        """
        Handle quick play for pile.

        Using auto piles attribute, perform best move and update stats.
        Return able perform.

        .. \quick_play(pile:Pile, event:Event=None) -> bool
        """
        nmoves = 0
        if CONF_OPT['quickplay'] and pile in self._autopiles(event)[2]:
            nmoves = pile.handle_quickplay(event)
            self._move['counter']['quickplay'] += nmoves
        return bool(nmoves)

    @property
    def __undo_assertions(self):
        """
        Check undo / redo actually posible.

        .. \__undo_assertions -> bool
        """
        move = self._move
        return (self.can_undo_move and move['state'] == self.S_PLAY and
                0 <= move['index'] <= len(move['history']))

    @state.move_state('undo')
    def _undo_move(self, move):
        """
        Undo given move sequence then append new snapshot.

        .. \_undo_move(move:seq(AtomicMove ...)) -> None
        """
        for atomic_move in move:
            atomic_move.undo(self)
        self._append_snapshot()

    def undo(self, sound=True):
        """
        Undo current move.

        Call undo move method for actual move. Update move stats then text
        items. Generate update menu event when not preview.

        .. undo(sound:bool=true) -> None

        *Variables:*
          moves : {map}
            Update current moves stats.

        *Raises*:
          AssertionError:
            When state not S_PLAY, no history, or index not in history.
        """
        if not self.can_undo_move:
            return
        assert self.__undo_assertions
        if sound:
            AUDIO_DEV.play('undo')
        move = self._move
        self.move_hint.reset()
        # undo move
        if move['index'] == 0:
            return
        move['index'] -= 1
        undo_move = move['history']['index'][:]
        undo_move.reverse()
        self._undo_move(undo_move)
        move['counter']['undo'] += 1
        # update view
        self._view_update('move')

    @state.move_state('redo')
    def _redo_move(self, move):
        """
        Redo given move sequence then append new snapshot.

        .. \_redo_move(move:seq(AtomicMove ...)) -> None
        """
        for atomic_move in move:
            atomic_move.redo(self)
        self._append_snapshot()

    def redo(self, sound=False):
        """
        Redo current move.

        Call redo move method for actual move. Update move stats then text items
        and finally check finished. Generate update menu event when not preview.

        .. redo([sound:bool=False]) -> None

        *Variables:*
          moves: {map}
            Update current moves stats.

        *Raises*:
          AssertionError:
            When state not S_PLAY, no history, or index not in history.
        """
        if not self.can_undo_move:
            return
        assert self.__undo_assertions
        if sound:
            AUDIO_DEV.play('redo')
        move = self._move
        if move['index'] == len(move['history']):
            return
        self.move_hint.reset()
        # redo move
        redo_move = move['history'][move['index']][:]
        self._redo_move(redo_move)
        move['index'] += 1
        move['counter']['redo'] += 1
        # update view
        self._view_update('move')
        self._check_finished()

    def redo_all(self):
        """
        Redo all move history.

        Call redo method until index is current or game finished.

        .. redo_all() -> None
        """
        move, finished = self._move, self.is_finish
        AUDIO_DEV.play('redo', loop=True)
        while move['index'] < len(move['history']) and not finished:
            self.redo()
        AUDIO_DEV.stop()

    @state.not_state('is_finish')
    def _check_finished(self):
        """
        Return game finished?.

        .. \_check_finished() -> bool
        """
        if not self.is_complete or self.talon.can_deal_cards():
            return False
        self.log()       # just in case
        self.move_state = self.S_FINISH
        self._timer_stop()
        if self.is_demo or self.is_preview:
            return True
        status, rank = self.is_success, ''
        if status in [1, 2]:
            cards = []
            for pile in self.allstacks:
                cards.extend(pile.cards)
            self.win_animation = AnimateWin(self.canvas,
                                            animate=CONF_OPT['animate_win'])
            self.win_animation.start(cards, perfect=status)
            # add rank stats info.
            rank = STAT_DB.position_text(CONF_OPT['player'], self)
        dlg = Dialog.finish_player(rank, status)
        self.top.event_generate('<<update_menus>>')
        if dlg.state == True and dlg.result in [0x10, 0x20]:
            self.new(restart=bool(dlg.result == 0x20))
        if dlg.state == False:    # timeout occured
            self.new()
            self.demo_start(mixed=True)
        return True

    @state.not_state('is_finish', 'is_pause')
    def view_match(self, match_type, match_info):
        """
        Show match for given type with given info.

        Update counter and call canvas method to show a match or no match. After
        type option timeout, remove canvas items added. Return has matches.

        .. \view_match(match_type:str, info:seq) -> bool

        *Parameters*:
          match_type : {str[piles|samerank|cards]}
            Highlight type.
          match_info : {sequence(sequence(Pile, Card, Card, str) ...)}
            Piles to highlight card sequences.
        """
        has_match = bool(len(match_info) == 0)
        if has_match:
            if CONF_OPT['mark_none']:
                self.canvas._create_no_match_item()
        else:
            self._move['counter'][match_type] += 1
            for info in match_info:
                self.canvas._create_match_item(info, self.images.size)
        self.top.update_idletasks()
        seconds = CONF_OPT['timeouts'][match_type]
        self.top.sleep(seconds)
        self.canvas._destroy_match_items()
        return has_match

    @state.not_state('is_finish', 'is_pause')
    def view_hint(self):
        """
        Show hint for next move.

        Wrapper that calls view hint method to get LineItem parameters.

        .. view_hint() -> None
        """
        if not CONF_OPT['hint'] or not self.move_hint.is_set:
            return
        movehint = self.move_hint.get(level=2 if self.is_demo else 0)
        hintitem = self._hint_item_(self, movehint, width=7,
                                    fill=CONF_OPT['colors']['hintarrow'])
        self.canvas.update_idletasks()
        seconds = CONF_OPT['timeouts']['hint']
        self.top.sleep(seconds)
        hintitem.delete()
        self._move['counter']['hints'] += 1

    def _append_snapshot(self):
        """
        Update snapshots for current pile state.

        .. \_append_snapshot() -> bool

        Generate hash state. Increment counter if not unique. Add unique hashes.
        """
        pic = hash('-'.join(''.join('{0[suit]}{0[rank]:03}{0.face_up}'.format(c)
                            for c in p.cards) for p in self.allstacks))
        self._move['counter']['dupe'] = pic in self._move['snapshots']
        self._move['snapshots'].add(pic)